import ResizeByEdgeAndCorner from "./simple-resize";
import "./style.css";

/**
 * 裁剪图片上传分三步：
    1. 预览
    2. 交互。交互可以用 canvas 来实时绘制，也可以用原生 dom 操作来实现。
    3. 上传
 */

// 傻逼了，为什么交互要用 canvas 画啊，交互的目的不就仅仅是要得到裁剪的坐标吗？
// 只是真正的裁剪操作需要 canvas，并且用 canvas 画出来预览并导出为 file 对象。

const selectImg = document.querySelector("#select-file") as HTMLInputElement;

// 裁剪容器
const cropBox = document.querySelector(".crop-box") as HTMLDivElement;
// 预览的完整底图
const cropPreviewImgEl = document.querySelector(".crop-preview-img") as HTMLImageElement;
// 蒙层
const maskEl = document.querySelector(".mask") as HTMLDivElement;
// 取景框图片
const viewportImgEL = document.querySelector(".crop-viewport-img") as HTMLImageElement;
// 取景框
const cropViewport = document.querySelector(".crop-viewport") as HTMLDivElement;

// 裁剪结果预览图
const previewImgEl = document.querySelector(".preview-img") as HTMLImageElement;

// 用于裁剪的 canvas，定义全局变量，避免重复创建。
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;

/**
 * canvas 裁剪图片。
 * @param img - 图片对象
 * @param x - 裁剪区域左上角 x 坐标
 * @param y - 裁剪区域左上角 y 坐标
 * @param w - 裁剪区域宽度
 * @param h - 裁剪区域高度
 * @returns 裁剪后的图片 dataUrl
 */
const _cropImgByCanvas = (img: HTMLImageElement, x: number, y: number, w: number, h: number) => {
    canvas.width = w;
    canvas.height = h;
    ctx.drawImage(img, x, y, w, h, 0, 0, w, h);
    const dataUrl = canvas.toDataURL("image/png");
    // const file = new File([dataUrl], "cropped.png", { type: "image/png" });
    // return { file, dataUrl };
    return { dataUrl };
};

/**
 * 依据相对 img 元素左上角的坐标裁剪图片。
 * @param cropViewportRelativeToImgLeft - 裁剪框相对 img 元素左上角的坐标
 * @param cropViewportRelativeToImgTop - 裁剪框相对 img 元素左上角的坐标
 * @param cropViewportWidth - 裁剪框宽度
 * @param cropViewportHeight - 裁剪框高度
 * @param zoom - 缩放比例：图片原始大小 / 显示大小
 * @returns 裁剪后的图片 dataUrl
 */
const _cropImg = (
    cropViewportRelativeToImgLeft: number,
    cropViewportRelativeToImgTop: number,
    cropViewportWidth: number,
    cropViewportHeight: number,
    zoom: number
) => {
    const cropNaturalLeft = cropViewportRelativeToImgLeft * zoom;
    const cropNaturalTop = cropViewportRelativeToImgTop * zoom;
    const cropNaturalWidth = cropViewportWidth * zoom;
    const cropNaturalHeight = cropViewportHeight * zoom;

    const { dataUrl } = _cropImgByCanvas(
        viewportImgEL,
        cropNaturalLeft,
        cropNaturalTop,
        cropNaturalWidth,
        cropNaturalHeight
    );
    return dataUrl;
};

/**
 * 将取景框大小同步到 img 元素并以此裁剪出取景框大小的图片
 * @param cropViewport - 取景框元素
 * @param imgRelativeToContainerLeft - 图片元素相对容器左上角的距离
 * @param imgRelativeToContainerTop - 图片元素相对容器左上角的距离
 * @returns 取景框相对 img 元素的坐标和取景框宽高
 */
const _syncSizeToCropImg = (
    cropViewport: HTMLElement,
    imgRelativeToContainerLeft: number,
    imgRelativeToContainerTop: number
) => {
    const { width: cropViewportWidth, height: cropViewportHeight } = cropViewport.getBoundingClientRect();
    const left = +cropViewport.style.left.split("px")[0];
    const top = +cropViewport.style.top.split("px")[0];

    // 注意：clip-path 坐标相对的是 img 元素的左上角，而获取到的取景框元素的坐标是相对容器的。因此要减去 img 元素与容器的“缝隙距离”

    // 取景框相对预览图 img 元素的坐标
    const cropViewportRelativeToImgLeft = left - imgRelativeToContainerLeft;
    const cropViewportRelativeToImgTop = top - imgRelativeToContainerTop;

    // 取景框的四个顶点坐标。
    const leftTopPoint = `${cropViewportRelativeToImgLeft}px ${cropViewportRelativeToImgTop}px`;
    const rightTopPoint = `${cropViewportRelativeToImgLeft + cropViewportWidth}px ${top - imgRelativeToContainerTop}px`;
    const rightBottomPoint = `${cropViewportRelativeToImgLeft + cropViewportWidth}px ${
        cropViewportRelativeToImgTop + cropViewportHeight
    }px`;
    const leftBottomPoint = `${cropViewportRelativeToImgLeft}px ${cropViewportRelativeToImgTop + cropViewportHeight}px`;

    // clip-path 裁剪 img 元素。
    viewportImgEL.style.clipPath = `polygon(${leftTopPoint}, ${rightTopPoint}, ${rightBottomPoint}, ${leftBottomPoint})`;

    // 返回取景框相对 img 元素的坐标和取景框大小，也就是返回取景框在 img 元素中的 getBoundingClientRect() 值。
    return {
        cropViewportRelativeToImgLeft,
        cropViewportRelativeToImgTop,
        cropViewportWidth,
        cropViewportHeight
    };
};

/**
 * 裁剪并预览图片
 * @param cropViewport - 取景框元素
 * @param imgRelativeToContainerLeft - 图片元素相对容器左上角的距离
 * @param imgRelativeToContainerTop - 图片元素相对容器左上角的距离
 * @param zoom - 缩放比例：图片原始大小 / 显示大小
 */
const cropAndPreview = (
    cropViewport: HTMLElement,
    imgRelativeToContainerLeft: number,
    imgRelativeToContainerTop: number,
    zoom: number
) => {
    // 取景框宽高。
    const { width: cropViewportWidth, height: cropViewportHeight } = cropViewport.getBoundingClientRect();
    // 将取景框大小同步到 img 元素并以此裁剪出取景框大小的图片
    const { cropViewportRelativeToImgTop, cropViewportRelativeToImgLeft } = _syncSizeToCropImg(
        cropViewport,
        imgRelativeToContainerLeft,
        imgRelativeToContainerTop
    );
    // 依据相对 img 元素左上角的坐标裁剪图片。
    const dataUrl = _cropImg(
        cropViewportRelativeToImgLeft,
        cropViewportRelativeToImgTop,
        cropViewportWidth,
        cropViewportHeight,
        zoom
    );
    // 预览裁剪结果
    previewImgEl.src = dataUrl;
};

selectImg.addEventListener("change", (e: any) => {
    const file = e.target.files[0];
    const url = URL.createObjectURL(file);

    // 图片预览：以背景图方式填充裁剪容器的方式实现。并设置背景图 contain 和 center ，在裁剪容器中居中。
    // cropBox.style.backgroundImage = `url(${url})`;
    // 背景图方式实现有个问题：
    // 当图片原尺寸小于容器尺寸时，图片作为背景图会放大填充背景。但取景框图片是用 img 元素引入的，img 元素会保持原有图片大小。
    // 结果就是取景框图片和蒙层会小于裁剪容器的背景图，无法实现两层图片的重叠覆盖。
    // 因此，放弃背景图作为底图的方式，同样改用 img 元素作为底图。
    // 结构：cropPreviewImg<img>底图 -> div蒙层 -> 裁剪viewportImgEL<img>得到的取景框图片 -> 取景框。
    cropPreviewImgEl.src = url;

    // 取景框图片：从 <img> 中裁剪出取景框大小的图片。
    // 注意，<img> 也要设置 contain 和 center 以便与底图重合。
    viewportImgEL.src = url;

    viewportImgEL.onload = () => {
        // 裁剪容器大小
        const { left: cropBoxLeft, top: cropBoxTop } = cropBox.getBoundingClientRect();
        // 覆盖的 img 元素大小
        // 图片显示大小。这里获取到的大小是裁剪后的大小，还是没裁剪的完整大小？
        // 答：是没有裁剪时的完整<img>元素的大小。这也说明 clip-path 并不会改变 <img> 元素大小，只是隐藏了部分内容。
        // <img>元素的大小也就是预览图的大小，因为 <img> 原本显示的图片与预览的底图重合。
        const {
            left: viewportImgLeft,
            top: viewportImgTop,
            width: viewportImgWidth,
            height: viewportImgHeight
        } = viewportImgEL.getBoundingClientRect();
        // 设置取景框默认大小。
        cropViewport.style.width = `${200}px`;
        cropViewport.style.height = `${200}px`;
        // 取景框初始到左上角。
        const { width: cropViewportWidth, height: cropViewportHeight } = cropViewport.getBoundingClientRect();

        // 图片原始大小。
        const { naturalWidth } = viewportImgEL;
        // 图片缩放比例。
        const zoom = naturalWidth / viewportImgWidth;

        // 取景框在裁剪容器中的定位。
        // 要注意：因为有子绝父相，取景框的 left top 定位相对的是容器盒子，而不是取景预览图 <img> 元素。
        // 但我们的目标却是限制取景框在 <img> 元素内移动。
        // 我们是在拿相对视口的坐标计算出相对裁剪容器盒子的坐标。

        // 计算预览图 img 元素相对裁剪容器的缝隙距离。
        const imgRelativeToContainerLeft = viewportImgLeft - cropBoxLeft;
        const imgRelativeToContainerTop = viewportImgTop - cropBoxTop;

        // 取景框移动的范围。
        const maxLeft = imgRelativeToContainerLeft + viewportImgWidth - cropViewportWidth;
        const maxTop = imgRelativeToContainerTop + viewportImgHeight - cropViewportHeight;
        const minLeft = imgRelativeToContainerLeft;
        const minTop = imgRelativeToContainerTop;

        // 初始化蒙层大小
        maskEl.style.display = "block";
        maskEl.style.left = `${imgRelativeToContainerLeft}px`;
        maskEl.style.top = `${imgRelativeToContainerTop}px`;
        maskEl.style.width = `${viewportImgWidth}px`;
        maskEl.style.height = `${viewportImgHeight}px`;

        // 取景框初始到左上角。
        cropViewport.style.left = `${minLeft}px`;
        cropViewport.style.top = `${minTop}px`;

        // 初始化裁剪图片并预览
        cropAndPreview(cropViewport, imgRelativeToContainerLeft, imgRelativeToContainerTop, zoom);

        // 拖动取景框
        cropViewport.addEventListener("mousedown", (e: MouseEvent) => {
            e.preventDefault();

            cropViewport.addEventListener("mousemove", handleMouseMove);
            cropViewport.addEventListener("mouseup", clearListeners);

            const { offsetX, offsetY } = e;

            function handleMouseMove(e: MouseEvent) {
                e.preventDefault();

                // 定义实现拖动
                const left = Math.max(minLeft, Math.min(maxLeft, e.clientX - cropBoxLeft - offsetX));
                const top = Math.max(minTop, Math.min(maxTop, e.clientY - cropBoxTop - offsetY));
                cropViewport.style.left = `${left}px`;
                cropViewport.style.top = `${top}px`;

                // 裁剪并预览图片
                cropAndPreview(cropViewport, imgRelativeToContainerLeft, imgRelativeToContainerTop, zoom);
            }

            function clearListeners() {
                cropViewport.removeEventListener("mousemove", handleMouseMove);
                cropViewport.removeEventListener("mouseup", clearListeners);
            }
        });

        // 观察取景框元素大小变化，实时更新取景框和预览图。
        const ob = new ResizeObserver((entries: ResizeObserverEntry[]) => {
            entries.forEach((entry: ResizeObserverEntry) => {
                const { target } = entry;
                if (target === cropViewport) {
                    cropAndPreview(cropViewport, imgRelativeToContainerLeft, imgRelativeToContainerTop, zoom);
                }
            });
        });
        ob.observe(cropViewport);
    };
});

// 拖拽调整截取框大小
new ResizeByEdgeAndCorner(cropViewport, cropBox);

// 保存裁剪结果
const saveBtn = document.querySelector(".upload") as HTMLButtonElement;
saveBtn.addEventListener("click", () => {
    const url = previewImgEl.src;
    const a = document.createElement("a");
    a.href = url;
    a.download = "cropped.png";
    a.click();
});
